package client;
import java.awt.Point;
import java.util.*;

import util.GameConstants;

public class GameModel {
	public Player[] players;
	public TreeMap<Integer, Projectile> projectiles; 
	public int projectileCount;
	public GameMap gameMap;
	public ArrayList<Collision> collisions;
	public ArrayList<SpellEffect> spellEffects;
	public ArrayList<Summon> summons;
	public int turnNumber;
	public GameModel() {
		startGame();
		
	}
	public void startGame() {
		initMap();
		players = new Player[2];
		players[0] = new Player(0, 50, 75 );
		players[1] = new Player(1, 50, 25 );
		
		projectiles = new TreeMap<Integer, Projectile>();
		projectileCount = 0;
		collisions = new ArrayList<Collision>();
		spellEffects = new ArrayList<SpellEffect>();
		summons = new ArrayList<Summon>();
		turnNumber = 1;
	}
	public void initMap() {
		gameMap = new GameMap(200, 100);
	}
	public void turnLoop() {
		//summons act
		actSummons();
		
		//apply terrain regen
		applyTerrainRegen();
		
		//modify timers
		for(Player pl: players) {
			pl.turnsUntilCanCast = Math.max(0, pl.turnsUntilCanCast-1);
			pl.turnsSinceLastDamaged++;
			ArrayList<PlayerCondition> conditionsToRemove = new ArrayList<PlayerCondition>();
			for(PlayerCondition pc: pl.conditions) {
				if(pc.turnExpire<=turnNumber)
					conditionsToRemove.add(pc);
			}
			for(PlayerCondition pc: conditionsToRemove) {
				pl.conditions.remove(pc);
			}
		}
		ArrayList<Collision> toRemove = new ArrayList<Collision>();
		for(Collision col: collisions) {
			col.turnsLeft--;
			if(col.turnsLeft<=0)
				toRemove.add(col);
		}
		for(Collision col: toRemove) {
			collisions.remove(col);
		}
		
		//compute motion and detect collisions in ministeps
		for(Player pl: players) {
			double moveSpeedMultiplier = 1.0;
			int hasteCount = 0;
			for(PlayerCondition pc: pl.conditions)
				if(pc.type==0)
					hasteCount++;
			double hasteMultiplier = 1+hasteCount/(1.0+hasteCount)*(GameConstants.hasteSpeedMultiplier-1);
			moveSpeedMultiplier*=hasteMultiplier;
			pl.xVel*=moveSpeedMultiplier;
			pl.yVel*=moveSpeedMultiplier;
		}
		for(Integer i:projectiles.keySet()) {
			Projectile p = projectiles.get(i);
			if(p.type==1) {
				int plID = getEnemyNearest(p.player, p.xPos, p.yPos, GameConstants.magicMissileHomeDistance);
				if(plID==-1) continue;
				Player pl = players[plID];
				double dx = pl.xLoc-p.xPos;
				double dy = pl.yLoc-p.yPos;
				double dist = Math.sqrt(dx*dx+dy*dy);
				double projSpeed = Math.sqrt(p.xVel*p.xVel+p.yVel*p.yVel);
				double angleA = Math.atan2(dy, dx);
				double angleB = Math.atan2(p.yVel, p.xVel);
				double angleToRotate = angleA-angleB;
				if(angleToRotate>Math.PI) angleToRotate-=2*Math.PI;
				if(angleToRotate<-Math.PI) angleToRotate+=2*Math.PI;
				if(Math.abs(angleToRotate)>Math.PI/2) continue;
				double maxRotatePerTurn = GameConstants.magicMissileHomeSpeed;
				if(Math.abs(angleToRotate)>maxRotatePerTurn) angleToRotate = Math.signum(angleToRotate)*maxRotatePerTurn;
				double finalAngle = angleToRotate+angleB;
				p.xVel = Math.cos(finalAngle)*projSpeed;
				p.yVel = Math.sin(finalAngle)*projSpeed;
			}
		}
		for(int step=0; step<GameConstants.miniStepsPerTurn; step++) {
			for(Integer i:projectiles.keySet()) {
				Projectile p = projectiles.get(i);
				p.xPos+=p.xVel/GameConstants.miniStepsPerTurn;
				p.yPos+=p.yVel/GameConstants.miniStepsPerTurn;
			}
			for(Player pl: players) {
				movePlayerBy(pl.id, pl.xVel/GameConstants.miniStepsPerTurn, pl.yVel/GameConstants.miniStepsPerTurn);
			}
			checkProjectileCollisions();
		}
		
		//process spell effects
		processSpellEffects();
		
		//increment turn number
		turnNumber++;
	}
	public void actSummons() {
		int size = summons.size();
		for(int i=size-1; i>=0; i--) { //iterate backwards because the summon objects may remove themselves from the list in this loop.
			summons.get(i).act(this);
		}
	}
	public void applyTerrainRegen() {
		for(Player pl: players) {
			if(pl.turnsSinceLastDamaged<GameConstants.terrainRegenDelay)
				continue;
			pl.fire=Math.min(GameConstants.maxMana, pl.fire+GameConstants.fireManaBaseRegen.get(gameMap.getTerrain((int)pl.xLoc, (int)pl.yLoc))/3);
			pl.light=Math.min(GameConstants.maxMana, pl.light+GameConstants.lightManaBaseRegen.get(gameMap.getTerrain((int)pl.xLoc, (int)pl.yLoc))/3);
			pl.ice=Math.min(GameConstants.maxMana, pl.ice+GameConstants.iceManaBaseRegen.get(gameMap.getTerrain((int)pl.xLoc, (int)pl.yLoc))/3);
		}
	}
	public void checkProjectileCollisions() {
		ArrayList<Integer> toRemove = new ArrayList<Integer>();
		for(Projectile pj: projectiles.values()) {
			if(!canFlyOver(pj.xPos, pj.yPos)) {
				collisions.add(new Collision(pj.xPos, pj.yPos, 0, GameConstants.blastDuration));
				toRemove.add(pj.id);
				continue;
			}
			int targetPlayer = getPlayerNearest(pj.xPos, pj.yPos, GameConstants.fireballSize);
			if(targetPlayer!=-1) {
				collisions.add(new Collision(pj.xPos, pj.yPos, 0, GameConstants.blastDuration));
				double damage = (new double[] {GameConstants.fireballDamage, GameConstants.sentinelShotDamage, GameConstants.magicMissileDamage})[pj.type];
				takeDamage(targetPlayer, damage);
				toRemove.add(pj.id);
				continue;
			}
			
		}
		for(Integer i: toRemove)
			projectiles.remove(i);
	}
	public void processSpellEffects() {
		ArrayList<SpellEffect> toRemove = new ArrayList<SpellEffect>();
		for(SpellEffect se: spellEffects) {
			if(se.spellType==2) {
				if(turnNumber>=se.turnCast+GameConstants.smiteDelay) {
					collisions.add(new Collision(se.xTarget, se.yTarget, 1, GameConstants.blastDuration));
					int plID = getPlayerNearest(se.xTarget, se.yTarget, GameConstants.smiteRadius);
					if(plID!=-1)
						takeDamage(plID, GameConstants.smiteDamage);
					toRemove.add(se);
				}
			} else if(se.spellType==3) {
				if(turnNumber>=se.turnCast+GameConstants.conflagurationDelay) {
					for(Player pl: players) {
						double dx = pl.xLoc-se.xTarget;
						double dy = pl.yLoc-se.yTarget;
						double dist = Math.sqrt(dx*dx+dy*dy);
						if(dist<=GameConstants.conflagurationRadius)
							takeDamage(pl.id, GameConstants.conflagurationDPT);
					}
				}	
				if(turnNumber>=se.turnCast+GameConstants.conflagurationDelay+GameConstants.conflagurationDuration)
					toRemove.add(se);
			}
		}
		for(SpellEffect se: toRemove)
			spellEffects.remove(se);
	}
	public boolean canCast(int playerID, int castID) {
		Player pl = players[playerID];
		if(pl.turnsUntilCanCast>0)
			return false;
		double fMinCost = GameConstants.fireManaCost[castID];
		double lMinCost = GameConstants.lightManaCost[castID];
		double iMinCost = GameConstants.iceManaCost[castID];
		if(!(pl.fire>=fMinCost && pl.light>=lMinCost && pl.ice>=iMinCost)) 
			return false;
		return true;
	}
	/** Casts a spell. Returns if the cast was successful. */
	public boolean castSpell(int playerID, int castID, double xTarget, double yTarget) {
		if(!canCast(playerID, castID))
			return false;
		Player pl = players[playerID];
		double costModifier = 1;
		if(castID==1) {
			double dx = xTarget-pl.xLoc;
			double dy = yTarget-pl.yLoc;
			double dist = Math.sqrt(dx*dx+dy*dy);
			costModifier*=Math.pow(dist, GameConstants.teleportManaCostExponent);
			if(costModifier<1) costModifier = 1;
		}
		double fCost = GameConstants.fireManaCost[castID]*costModifier;
		double lCost = GameConstants.lightManaCost[castID]*costModifier;
		double iCost = GameConstants.iceManaCost[castID]*costModifier;
		if(!(pl.fire>=fCost && pl.light>=lCost && pl.ice>=iCost)) 
			return false;
		
		if(castID==0) { //fireball
			addProjectile(playerID, players[playerID].xLoc, players[playerID].yLoc, 0, xTarget, yTarget, GameConstants.fireballSpeed);
		} else if(castID==1) { //teleport
			movePlayerTo(playerID, xTarget, yTarget);
		} else if(castID==2) { //smite
			spellEffects.add(new SpellEffect(playerID, turnNumber, 2, xTarget, yTarget));
		} else if(castID==3) { //conflag
			spellEffects.add(new SpellEffect(playerID, turnNumber, 3, xTarget, yTarget));
		} else if(castID==4) { //sentinel
			if(!this.canMoveTo(xTarget, yTarget))
				return false;
			summons.add(new FireSentinel(xTarget, yTarget, playerID));
		} else if(castID==5) { //haste
			players[playerID].conditions.add(new PlayerCondition(0, turnNumber, turnNumber+GameConstants.hasteDuration));
		} else if(castID==6) { //magicmissile
			addProjectile(playerID, players[playerID].xLoc, players[playerID].yLoc, 1, xTarget, yTarget, GameConstants.magicMissileSpeed);
		}
		
		pl.fire-=fCost;
		pl.light-=lCost;
		pl.ice-=iCost;
		pl.turnsUntilCanCast = GameConstants.castDelay;
		return true;
	}
	public void addProjectile(int playerID, double xStart, double yStart, int type, double xTarget, double yTarget, double speed) {
		int projectileID = projectileCount;
		double dx = xTarget-xStart;
		double dy = yTarget-yStart;
		double d = Math.sqrt(dx*dx+dy*dy);
		double pjSize = GameConstants.fireballSize;
		Projectile p = new Projectile(projectileID, playerID, type, xStart+pjSize*dx/d, yStart+pjSize*dy/d, 
				speed*dx/d, speed*dy/d);
		projectiles.put(projectileID, p);
		projectileCount++;
	}
	public void takeDamage(int playerID, double damage) {
		if(damage<=0) return;
		Player pl = players[playerID];
		pl.turnsSinceLastDamaged = 0;
		pl.fire=Math.max(0, pl.fire-damage/3);
		pl.light=Math.max(0, pl.light-damage/3);
		pl.ice=Math.max(0, pl.ice-damage/3);
	}
	public void healDamage(int playerID, double damage) {
		if(damage<=0) return;
		Player pl = players[playerID];
		pl.fire=Math.min(GameConstants.maxMana, pl.fire+damage/3);
		pl.light=Math.min(GameConstants.maxMana, pl.light+damage/3);
		pl.ice=Math.min(GameConstants.maxMana, pl.ice+damage/3);
	}
	public void setPlayerVelocity(int playerID, double x, double y) {
		players[playerID].xVel = x;
		players[playerID].yVel = y;
	}
	/** Can a player move to the given coords? */
	public boolean canMoveTo(double x, double y) {
		TerrainType tt = gameMap.getTerrain((int)x, (int)y);
		if( tt!=TerrainType.LAND ) return false;
		return true;
	}
	/** Can a flying object move to the given coords? */
	public boolean canFlyOver(double x, double y) {
		TerrainType tt = gameMap.getTerrain((int)x, (int)y);
		if( tt==TerrainType.OUT_OF_BOUNDS || tt==TerrainType.WALL ) return false;
		return true;
	}
	/** Are the given coords within the boundaries of the map? */
	public boolean inBounds(double x, double y) {
		TerrainType tt = gameMap.getTerrain((int)x, (int)y);
		if( tt==TerrainType.OUT_OF_BOUNDS ) return false;
		return true;
	}
	/** Returns the ID of the player at the given grid. Returns -1 if there is nobody there.*/
	public int getPlayerAt(int x, int y) {
		for(Player pl: players)
			if(((int)pl.xLoc)==x && ((int)pl.yLoc)==y) 
				return pl.id;
		return -1;
	}
	/** Returns the ID of the player closest to the given coord that is at most a certain distance away and also an enemy of the given player. 
	 * Returns -1 if there are no such players.*/
	public int getEnemyNearest(int playerID, double x, double y, double maxDistance) {
		int best = -1;
		double bestDist = Double.MAX_VALUE;
		for(Player pl: players) {
			if(pl.id==playerID)
				continue;
			double dx = x-pl.xLoc;
			double dy = y-pl.yLoc;
			double dist = Math.sqrt(dx*dx+dy*dy);
			if(dist<bestDist) { // breaks ties by returning player with lowest id, TODO change to break ties more fairly
				best = pl.id;
				bestDist = dist; 
			}
		}
		if(bestDist>maxDistance)
			return -1;
		return best;
	}
	/** Returns the ID of the player closest to the given coord that is at most a certain distance away. 
	 * Returns -1 if there are no such players.*/
	public int getPlayerNearest(double x, double y, double maxDistance) {
		int best = -1;
		double bestDist = Double.MAX_VALUE;
		for(Player pl: players) {
			double dx = x-pl.xLoc;
			double dy = y-pl.yLoc;
			double dist = Math.sqrt(dx*dx+dy*dy);
			if(dist<bestDist) { // breaks ties by returning player with lowest id, TODO change to break ties more fairly
				best = pl.id;
				bestDist = dist; 
			}
		}
		if(bestDist>maxDistance)
			return -1;
		return best;
	}
	/** Tries to move the player to the given coords. Returns whether the movement was successful. */
	public boolean movePlayerTo(int playerID, double x, double y) {
		if(!canMoveTo(x,y)) return false;
		int playerAtDest = getPlayerAt((int)x, (int)y);
		if(playerAtDest!=-1 && playerAtDest!=playerID) return false;
		
		players[playerID].xLoc = x;
		players[playerID].yLoc = y;
		return true;
	}
	/** Tries to move the player by the given vector <dx,dy>. Returns whether the movement was successful. */
	public boolean movePlayerBy(int playerID, double dx, double dy) {
		return movePlayerTo(playerID, players[playerID].xLoc+dx, players[playerID].yLoc+dy);
	}
}
